package engine.gameObject.terrain;

import engine.gameObject.GameObject;
import engine.util.model.Material;

import java.nio.FloatBuffer;

import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;

import static engine.util.vbo.VBO.*;
import static org.lwjgl.opengl.GL11.GL_CULL_FACE;
import static org.lwjgl.opengl.GL11.GL_FLOAT;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_2D;
import static org.lwjgl.opengl.GL11.GL_TRIANGLES;
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_INT;
import static org.lwjgl.opengl.GL11.glColor3f;
import static org.lwjgl.opengl.GL11.glDisable;
import static org.lwjgl.opengl.GL11.glDrawElements;
import static org.lwjgl.opengl.GL11.glEnable;
import static org.lwjgl.opengl.GL11.glNormalPointer;
import static org.lwjgl.opengl.GL11.glTexCoordPointer;
import static org.lwjgl.opengl.GL11.glVertexPointer;

/**
 * This class represents a game object which is containing a terrain. It is necessary, that the sides of the terrain has a power of two. 
 * @author Richard
 *
 */
public class Terrain extends GameObject{
	public static final Material MATERIAL = new Material(new float[] { 0.3f,
			0.3f, 0.3f, 1 }, new float[] { 0.7f, 0.7f, 0.7f, 1 }, null, null, 0);
	
	private int width;
	private int depth;
	private int detailAreaSize;		//the size of a area for loding.
	private int[][][] indicesStorage; //stores the indices for the different levels of detail and their positions. 
	private float[][] heightMap;
	private int[][] basicTerrainVBO;
	private int[][][] renderIndices;
	private int stride;
	
	private int highDetailArea = 2; // the area around the player, which is drawn with high detail, minimum is 3
	private int highDetailStep = 1; // the steps, when the detail changes from higher to lower
	
	private int lastAreaX = Integer.MIN_VALUE; // stores the last area location, where the camera currently is
	private int lastAreaZ = Integer.MIN_VALUE; // stores the last area location, where the camera currently is
	
	/**
	 * The (width - 1) and (height - 1) of the heightMap array must have power of two. The areaSize specifies the size of areas, for the level of detail. The areaSize must have a power of two too. 
	 * @param heightMap
	 * @param areaSize
	 */
	public Terrain(float[][] heightMap, int areaSize) {
		width = heightMap.length;
		depth = heightMap[0].length;
		this.heightMap = heightMap;
		this.detailAreaSize = areaSize;
		
		if(!isPowerOfTwo(width) || !isPowerOfTwo(depth) || !isPowerOfTwo(areaSize)) {
			throw new NullPointerException("The sizes of the array and the size of areaSize must be powers of two");
		}
		
		if(areaSize >= width || areaSize >= depth) {
			throw new NullPointerException("Area size must be smaller than width and depth");
		}
		
		buildVBOs();
		setInNearPane(false);
		setMarkAsLarge(true);
	}
	
	/**
	 * Builds the VBO's for the several sections.
	 * 
	 * @param height
	 */
	private void buildVBOs() {
		int sizeX = width - 1;
		int sizeZ = depth -1;
		int areaCountX = (width - 1) / detailAreaSize;
		int areaCountZ = (depth - 1) / detailAreaSize;

		float[][][] normals = computeNormals(heightMap);
		float[][][] octreePoints = new float[areaCountX][areaCountZ][2];
		
		float[] min = new float[3];
		float[] max = new float[3];

		basicTerrainVBO = new int[areaCountX][areaCountZ];

		for (int i = 0; i < areaCountX; i++) {
			for (int j = 0; j < areaCountZ; j++) {
				FloatBuffer mapData = BufferUtils.createFloatBuffer(8
						* (detailAreaSize + 1) * (detailAreaSize + 1));

				octreePoints[i][j][0] = heightMap[i * detailAreaSize][j * detailAreaSize];
				octreePoints[i][j][1] = heightMap[i * detailAreaSize][j * detailAreaSize];

				for (int x = i * detailAreaSize; x <= (i + 1) * detailAreaSize; x++) {
					for (int z = j * detailAreaSize; z <= (j + 1) * detailAreaSize; z++) {
						mapData.put(x);
						mapData.put(heightMap[x][z]);
						mapData.put(z);
						mapData.put(x / (float) sizeX);
						mapData.put(z / (float) sizeZ);
						mapData.put(normals[x][z][0]);
						mapData.put(normals[x][z][1]);
						mapData.put(normals[x][z][2]);

						if (heightMap[x][z] < octreePoints[i][j][0]) {
							octreePoints[i][j][0] = heightMap[x][z];
						}

						if (heightMap[x][z] > octreePoints[i][j][1]) {
							octreePoints[i][j][1] = heightMap[x][z];
						}
					}
				}

				mapData.rewind();
				basicTerrainVBO[i][j] = glGenBuffers();

				glBindBuffer(basicTerrainVBO[i][j]);
				glBufferData(basicTerrainVBO[i][j], mapData);
				glBindBuffer(0);

				mapData.clear();
				
				if(min[1] > octreePoints[i][j][0]) {
					min[1] = octreePoints[i][j][0];
				}
				
				if(max[1] < octreePoints[i][j][1]) {
					max[1] = octreePoints[i][j][1];
				}
			}
		}
		
		setBounds(0, 0, 0, Math.sqrt(width / 2 * width / 2 + (max[1] - min[1]) / 2 * (max[1] - min[1]) / 2 + depth / 2 * depth / 2));

		createIndiciesList();

		renderIndices = new int[areaCountX][areaCountZ][3];

		stride = (3 + 2 + 3) * 4;
	}
	
	/**
	 * Creates the indices storage lists for LODing
	 */
	private void createIndiciesList() {
		indicesStorage = new int[6][][];

		for (int i = 1, index = 0; i < detailAreaSize; i *= 2, index++) {
			indicesStorage[index] = new int[9][2];

			indicesStorage[index][0][0] = IndiciesCreator.getIndicies1(i, detailAreaSize);
			indicesStorage[index][0][1] = IndiciesCreator.getLastLength();

			indicesStorage[index][1][0] = IndiciesCreator.getIndicies2(i, detailAreaSize);
			indicesStorage[index][1][1] = IndiciesCreator.getLastLength();

			indicesStorage[index][2][0] = IndiciesCreator.getIndicies3(i, detailAreaSize);
			indicesStorage[index][2][1] = IndiciesCreator.getLastLength();

			indicesStorage[index][3][0] = IndiciesCreator.getIndicies4(i, detailAreaSize);
			indicesStorage[index][3][1] = IndiciesCreator.getLastLength();

			indicesStorage[index][4][0] = IndiciesCreator.getIndicies5(i, detailAreaSize);
			indicesStorage[index][4][1] = IndiciesCreator.getLastLength();

			indicesStorage[index][5][0] = IndiciesCreator.getIndicies6(i, detailAreaSize);
			indicesStorage[index][5][1] = IndiciesCreator.getLastLength();

			indicesStorage[index][6][0] = IndiciesCreator.getIndicies7(i, detailAreaSize);
			indicesStorage[index][6][1] = IndiciesCreator.getLastLength();

			indicesStorage[index][7][0] = IndiciesCreator.getIndicies8(i, detailAreaSize);
			indicesStorage[index][7][1] = IndiciesCreator.getLastLength();

			indicesStorage[index][8][0] = IndiciesCreator.getIndicies9(i, detailAreaSize);
			indicesStorage[index][8][1] = IndiciesCreator.getLastLength();
		}

		indicesStorage[5] = new int[1][2];
		indicesStorage[5][0][0] = IndiciesCreator.getIndicies5(detailAreaSize, detailAreaSize);
		indicesStorage[5][0][1] = IndiciesCreator.getLastLength();
	}
	
	/**
	 * Computes the normals to the map.
	 * 
	 * @param height
	 * @return
	 */
	private float[][][] computeNormals(float[][] height) {
		int size = height.length - 1;
		float[][][] normals = new float[height.length][height.length][3];

		for (int posX = 0; posX < normals.length; posX++) {
			for (int posZ = 0; posZ < normals[posX].length; posZ++) {
				float x = 0.5f * (height[Math.max(0, posX - 1)][posZ] - height[Math
						.min(size, posX + 1)][posZ]);
				float y = 1;
				float z = 0.5f * (height[posX][Math.max(0, posZ - 1)] - height[posX][Math
						.min(size, posZ + 1)]);
				float length = (float) Math.sqrt(x * x + y * y + z * z);

				normals[posX][posZ][0] = x / length;
				normals[posX][posZ][1] = y / length;
				normals[posX][posZ][2] = z / length;
			}
		}

		return normals;
	}
	
	private boolean isPowerOfTwo(int number) {
		for(int i = 1; i <= number; i *= 2) {
			if(i == number) {
				return true;
			}
		}
		
		return false;
	}
	
	@Override
	public void render(float timeElapsed) {
		updateLodingIndices();
		
		renderTerrain();
	}
	
	private void updateLodingIndices() {
		int x = (int) (getScene().getCurrentCamera().getEyeX() + (highDetailArea + 1)
				% 2 * detailAreaSize / 2)
				/ detailAreaSize;
		int z = (int) (getScene().getCurrentCamera().getEyeZ() + (highDetailArea + 1)
				% 2 * detailAreaSize / 2)
				/ detailAreaSize;

		if (x != lastAreaX || z != lastAreaZ) {
			lastAreaX = x;
			lastAreaZ = z;

			float fx = x - (highDetailArea + 1) % 2 * 0.5f;
			float fz = z - (highDetailArea + 1) % 2 * 0.5f;

			for (int i = 0; i < renderIndices.length; i++) {
				for (int j = 0; j < renderIndices[i].length; j++) {
					boolean alreadySet = false;
					
					for(int detail = 0; detail < exponentOf(detailAreaSize); detail++) {
						if (Math.abs(fx - i) < highDetailArea / 2f + detail
								* highDetailStep
								&& Math.abs(fz - j) < highDetailArea / 2f + detail
										* highDetailStep) {
							updateVBO(x, z, i, j, detail);
							alreadySet = true;
							break;
						}
					}
					
					if(!alreadySet) {
						renderIndices[i][j][0] = indicesStorage[5][0][0];
						renderIndices[i][j][1] = indicesStorage[5][0][1];
						renderIndices[i][j][2] = exponentOf(detailAreaSize);
					}
				}
			}
		}
	}
	
	private void updateVBO(int x, int z, int i, int j, int detail) {
		int borderRight = z + (highDetailArea - (highDetailArea + 1) % 2) / 2
				+ detail * highDetailStep;
		int borderLeft = z - highDetailArea / 2 - detail * highDetailStep;
		int borderTop = x + (highDetailArea - (highDetailArea + 1) % 2) / 2
				+ detail * highDetailStep;
		int borderBottom = x - highDetailArea / 2 - detail * highDetailStep;

		renderIndices[i][j][2] = detail;

		if (j == borderRight) {
			if (i == borderBottom) {
				renderIndices[i][j][0] = indicesStorage[detail][8][0];
				renderIndices[i][j][1] = indicesStorage[detail][8][1];
			} else if (i == borderTop) {
				renderIndices[i][j][0] = indicesStorage[detail][2][0];
				renderIndices[i][j][1] = indicesStorage[detail][2][1];
			} else {
				renderIndices[i][j][0] = indicesStorage[detail][5][0];
				renderIndices[i][j][1] = indicesStorage[detail][5][1];
			}
		} else if (j == borderLeft) {
			if (i == borderBottom) {
				renderIndices[i][j][0] = indicesStorage[detail][6][0];
				renderIndices[i][j][1] = indicesStorage[detail][6][1];
			} else if (i == borderTop) {
				renderIndices[i][j][0] = indicesStorage[detail][0][0];
				renderIndices[i][j][1] = indicesStorage[detail][0][1];
			} else {
				renderIndices[i][j][0] = indicesStorage[detail][3][0];
				renderIndices[i][j][1] = indicesStorage[detail][3][1];
			}
		} else if (i == borderBottom) {
			renderIndices[i][j][0] = indicesStorage[detail][7][0];
			renderIndices[i][j][1] = indicesStorage[detail][7][1];
		} else if (i == borderTop) {
			renderIndices[i][j][0] = indicesStorage[detail][1][0];
			renderIndices[i][j][1] = indicesStorage[detail][1][1];
		} else {
			renderIndices[i][j][0] = indicesStorage[detail][4][0];
			renderIndices[i][j][1] = indicesStorage[detail][4][1];
		}
	}
	
	private int exponentOf(int number) {
		for(int i = 0, exp = 1; exp <= number; i++, exp *= 2) {
			if(exp == number) {
				return i;
			}
		}
		
		return -1;
	}
	
	private void renderTerrain() {
		MATERIAL.bind();
		
		// glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
		glEnable(GL_CULL_FACE);
//		glEnable(GL_TEXTURE_2D);
//		GL13.glActiveTexture(GL13.GL_TEXTURE0);
//		glBindTexture(GL_TEXTURE_2D, texture);
		glColor3f(0.5f, 0.5f, 0.5f);

		GL11.glEnableClientState(GL11.GL_VERTEX_ARRAY);
		GL11.glEnableClientState(GL11.GL_NORMAL_ARRAY);
		GL11.glEnableClientState(GL11.GL_TEXTURE_COORD_ARRAY);

		glColor3f(1, 1, 1);
		for (int i = 0; i < renderIndices.length; i++) {
			for (int j = 0; j < renderIndices.length; j++) {
				glBindBuffer(basicTerrainVBO[i][j]);
				glVertexPointer(3, GL_FLOAT, stride, 0);
				glTexCoordPointer(2, GL_FLOAT, stride, 12);
				glNormalPointer(GL_FLOAT, stride, 20);

				glBindElementBuffer(renderIndices[i][j][0]);
				glDrawElements(GL_TRIANGLES, renderIndices[i][j][1], GL_UNSIGNED_INT, 0);
			}
		}

		glBindBuffer(0);
		glBindBuffer(0);
		glBindBuffer(0);

		GL11.glDisableClientState(GL11.GL_VERTEX_ARRAY);
		GL11.glDisableClientState(GL11.GL_NORMAL_ARRAY);
		GL11.glDisableClientState(GL11.GL_TEXTURE_COORD_ARRAY);

		glDisable(GL_TEXTURE_2D);
		glDisable(GL_CULL_FACE);
	}
}

